package com.hqd.ch03.v50.boot.context.properties;

import com.hqd.ch03.utils.ClassUtils;
import com.hqd.ch03.utils.ReflectionUtils;
import com.hqd.ch03.v50.aop.support.AopUtils;
import com.hqd.ch03.v50.beans.factory.NoSuchBeanDefinitionException;
import com.hqd.ch03.v50.beans.factory.config.BeanDefinition;
import com.hqd.ch03.v50.beans.factory.config.ConfigurableListableBeanFactory;
import com.hqd.ch03.v50.boot.context.properties.bind.Bindable;
import com.hqd.ch03.v50.context.ApplicationContext;
import com.hqd.ch03.v50.context.ConfigurableApplicationContext;
import com.hqd.ch03.v50.context.validation.Validated;
import com.hqd.ch03.v50.core.ResolvableType;
import com.hqd.ch03.v50.core.annotation.MergedAnnotation;
import com.hqd.ch03.v50.core.annotation.MergedAnnotations;
import com.hqd.ch03.v50.factory.support.RootBeanDefinition;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

public final class ConfigurationPropertiesBean {

    private final String name;

    private final Object instance;

    private final ConfigurationProperties annotation;

    private final Bindable<?> bindTarget;

    private final BindMethod bindMethod;

    private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
                                        Bindable<?> bindTarget) {
        this.name = name;
        this.instance = instance;
        this.annotation = annotation;
        this.bindTarget = bindTarget;
        this.bindMethod = BindMethod.forType(bindTarget.getType().resolve());
    }

    /**
     * Return all {@link ConfigurationProperties @ConfigurationProperties} beans contained
     * in the given application context. Both directly annotated beans, as well as beans
     * that have {@link ConfigurationProperties @ConfigurationProperties} annotated
     * factory methods are included.
     *
     * @param applicationContext the source application context
     * @return a map of all configuration properties beans keyed by the bean name
     */
    public static Map<String, ConfigurationPropertiesBean> getAll(ApplicationContext applicationContext) {
        if (applicationContext instanceof ConfigurableApplicationContext) {
            return getAll((ConfigurableApplicationContext) applicationContext);
        }
        Map<String, ConfigurationPropertiesBean> propertiesBeans = new LinkedHashMap<>();
        applicationContext.getBeansWithAnnotation(ConfigurationProperties.class).forEach((beanName, bean) -> {
            ConfigurationPropertiesBean propertiesBean = get(applicationContext, bean, beanName);
            if (propertiesBean != null) {
                propertiesBeans.put(beanName, propertiesBean);
            }
        });
        return propertiesBeans;
    }

    private static Map<String, ConfigurationPropertiesBean> getAll(ConfigurableApplicationContext applicationContext) {
        Map<String, ConfigurationPropertiesBean> propertiesBeans = new LinkedHashMap<>();
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        Iterator<String> beanNames = beanFactory.getBeanNamesIterator();
        while (beanNames.hasNext()) {
            String beanName = beanNames.next();
            if (isConfigurationPropertiesBean(beanFactory, beanName)) {
                try {
                    Object bean = beanFactory.getBean(beanName);
                    ConfigurationPropertiesBean propertiesBean = get(applicationContext, bean, beanName);
                    if (propertiesBean != null) {
                        propertiesBeans.put(beanName, propertiesBean);
                    }
                } catch (Exception ex) {
                }
            }
        }
        return propertiesBeans;
    }

    private static boolean isConfigurationPropertiesBean(ConfigurableListableBeanFactory beanFactory, String beanName) {
        try {
            if (beanFactory.getBeanDefinition(beanName).isAbstract()) {
                return false;
            }
            if (beanFactory.findAnnotationOnBean(beanName, ConfigurationProperties.class) != null) {
                return true;
            }
            Method factoryMethod = findFactoryMethod(beanFactory, beanName);
            return findMergedAnnotation(factoryMethod, ConfigurationProperties.class).isPresent();
        } catch (NoSuchBeanDefinitionException ex) {
            return false;
        }
    }

    /**
     * Return a {@link ConfigurationPropertiesBean @ConfigurationPropertiesBean} instance
     * for the given bean details or {@code null} if the bean is not a
     * {@link ConfigurationProperties @ConfigurationProperties} object. Annotations are
     * considered both on the bean itself, as well as any factory method (for example a
     * {@link Bean @Bean} method).
     *
     * @param applicationContext the source application context
     * @param bean               the bean to consider
     * @param beanName           the bean name
     * @return a configuration properties bean or {@code null} if the neither the bean nor
     * factory method are annotated with
     * {@link ConfigurationProperties @ConfigurationProperties}
     */
    public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
        Method factoryMethod = findFactoryMethod(applicationContext, beanName);
        return create(beanName, bean, bean.getClass(), factoryMethod);
    }

    private static Method findFactoryMethod(ApplicationContext applicationContext, String beanName) {
        if (applicationContext instanceof ConfigurableApplicationContext) {
            return findFactoryMethod((ConfigurableApplicationContext) applicationContext, beanName);
        }
        return null;
    }

    private static Method findFactoryMethod(ConfigurableApplicationContext applicationContext, String beanName) {
        return findFactoryMethod(applicationContext.getBeanFactory(), beanName);
    }

    private static Method findFactoryMethod(ConfigurableListableBeanFactory beanFactory, String beanName) {
        if (beanFactory.containsBeanDefinition(beanName)) {
            BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
            if (beanDefinition instanceof RootBeanDefinition) {
                Method resolvedFactoryMethod = ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod();
                if (resolvedFactoryMethod != null) {
                    return resolvedFactoryMethod;
                }
            }
            return findFactoryMethodUsingReflection(beanFactory, beanDefinition);
        }
        return null;
    }

    private static Method findFactoryMethodUsingReflection(ConfigurableListableBeanFactory beanFactory,
                                                           BeanDefinition beanDefinition) {
        String factoryMethodName = beanDefinition.getFactoryMethodName();
        String factoryBeanName = beanDefinition.getFactoryBeanName();
        if (factoryMethodName == null || factoryBeanName == null) {
            return null;
        }
        Class<?> factoryType = beanFactory.getType(factoryBeanName);
        if (factoryType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
            factoryType = factoryType.getSuperclass();
        }
        AtomicReference<Method> factoryMethod = new AtomicReference<>();
        ReflectionUtils.doWithMethods(factoryType, (method) -> {
            if (method.getName().equals(factoryMethodName)) {
                factoryMethod.set(method);
            }
        });
        return factoryMethod.get();
    }

    static ConfigurationPropertiesBean forValueObject(Class<?> beanClass, String beanName) {
        ConfigurationPropertiesBean propertiesBean = create(beanName, null, beanClass, null);
        return propertiesBean;
    }

    private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
        ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
        if (annotation == null) {
            return null;
        }
        Validated validated = findAnnotation(instance, type, factory, Validated.class);
        Annotation[] annotations = (validated != null) ? new Annotation[]{annotation, validated}
                : new Annotation[]{annotation};
        ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
                : ResolvableType.forClass(type);
        Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
        if (instance != null) {
            bindTarget = bindTarget.withExistingValue(instance);
        }
        return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
    }

    private static <A extends Annotation> A findAnnotation(Object instance, Class<?> type, Method factory,
                                                           Class<A> annotationType) {
        MergedAnnotation<A> annotation = MergedAnnotation.missing();
        if (factory != null) {
            annotation = findMergedAnnotation(factory, annotationType);
        }
        if (!annotation.isPresent()) {
            annotation = findMergedAnnotation(type, annotationType);
        }
        if (!annotation.isPresent() && AopUtils.isAopProxy(instance)) {
            annotation = MergedAnnotations.from(AopUtils.getTargetClass(instance), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
                    .get(annotationType);
        }
        return annotation.isPresent() ? annotation.synthesize() : null;
    }

    private static <A extends Annotation> MergedAnnotation<A> findMergedAnnotation(AnnotatedElement element,
                                                                                   Class<A> annotationType) {
        return (element != null) ? MergedAnnotations.from(element, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(annotationType)
                : MergedAnnotation.missing();
    }

    /**
     * Return the name of the Spring bean.
     *
     * @return the bean name
     */
    public String getName() {
        return this.name;
    }

    /**
     * Return the actual Spring bean instance.
     *
     * @return the bean instance
     */
    public Object getInstance() {
        return this.instance;
    }

    /**
     * Return the bean type.
     *
     * @return the bean type
     */
    public Class<?> getType() {
        return this.bindTarget.getType().resolve();
    }

    /**
     * Return the property binding method that was used for the bean.
     *
     * @return the bind type
     */
    public BindMethod getBindMethod() {
        return this.bindMethod;
    }

    /**
     * Return the {@link ConfigurationProperties} annotation for the bean. The annotation
     * may be defined on the bean itself or from the factory method that create the bean
     * (usually a {@link Bean @Bean} method).
     *
     * @return the configuration properties annotation
     */
    public ConfigurationProperties getAnnotation() {
        return this.annotation;
    }

    /**
     * Return a {@link Bindable} instance suitable that can be used as a target for the
     * {@link Binder}.
     *
     * @return a bind target for use with the {@link Binder}
     */
    public Bindable<?> asBindTarget() {
        return this.bindTarget;
    }

    /**
     * The binding method that is used for the bean.
     */
    public enum BindMethod {

        /**
         * Java Bean using getter/setter binding.
         */
        JAVA_BEAN,

        /**
         * Value object using constructor binding.
         */
        VALUE_OBJECT;

        static BindMethod forType(Class<?> type) {
            return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type, false) != null)
                    ? VALUE_OBJECT : JAVA_BEAN;
        }

    }

}